import multiprocessing as mp
import logging
import time
from Queue import Empty
import sys
import datetime
import Queue
from PyQt4.QtCore import *
import threading
import navalasystem as na
from misc import misc
import threads

class ProcessMonitor(threads.LoopThread, QObject):
    """Thread that keeps listening to running processes.
    
    Tasks:
        Register process
        Unregister process
        Maintain a list of registered processes
        Catch messages from processes and emit Qt signals
    """
    __counter = 0

    # Format:
    #   {processObject0: {"id: id0, "timeStarted": timeStarted0, 
    #                     "signals": [...]}, 
    #    ...}
    processes = {}
    
    # This is automatically initialized to a mp.Queue() . This queue is checked
    # in the thread loop for messages coming from any process.
    # Messages arriving here must be tuples like (id, token, data)
    # TODO: compose signal logic
    processQueue = None
    
    # This signal is useful to refresh some status GUI
    ProcessesChanged = pyqtSignal()
    
    L = threading.Lock()
    M = threading.Lock()
    
    def __init__(self, *args, **kwargs):
        threads.LoopThread.__init__(self, name="Process Monitor", *args, **kwargs)
        QObject.__init__(self)
        self.processQueue = mp.Queue()

    def AssertStarted(self):
        if not self.is_alive():
            self.Start()
            
    def process_Register(self, process_):
        """Registers process. Must be called before the process starts.
        
        This routine auto-starts the monitor if self.is_alive() returns false."""
        with self.L:
            self.AssertStarted()

            assert not self.processes.has_key(process_), \
                   """Process "%s" already registered!""" % process_.name
            (name, data) = self.__process_GetByName(process_.name)
            assert name is None, 'A process named "%s" is already registered!' % \
                process_.name
            #assert not process_.is_alive(), """Process "%s" is already running!"""
            flagOk = False

            process_.outQueue = self.processQueue
            process_.id = self.__counter
            self.processes[process_] = {"id": self.__counter,
                "timeStarted": datetime.datetime.today(),
                "signals": []}
            self.logger.info("Registered process %s (#%d)" % 
                             (process_.name, self.__counter))
            self.__counter += 1
            flagOk = True
        if flagOk:
            self.ProcessesChanged.emit() # Have to unlock L before emitting
            self.logger.debug("---------> Emited self.ProcessesChanged.emit()")

    def process_Unregister(self, process_):
        assert self.processes.has_key(process_), \
               """Process "%s" NOT registered!""" % process_.name
        with self.L:
            data = self.processes.pop(process_)
            self.logger.info("Popped process %s (#%d)" % (process_.name, data["id"]))
            self.ProcessesChanged.emit()
            
    def process_RegisterSlot(self, process_, slot):
        """Registers a slot function to receive messages from given process.
        
        Creates a signal, connects to slot and registers within process data.
        process_ must be already registered (TODO: this may be a problem as I may miss some message)."""
        assert self.processes.has_key(process_), \
               """Process "%s" NOT registered!""" % process_.name
        with self.L:
#            signal = pyqtSignal(list)
#            signal.connect(slot)
#            self.processes[process_]["signals"].append(signal)

# TODO: gonna try using callbacks because seems that creating signals on-the-fly is more complicated than I thought. the TODO is because the variables have wrong names now (i.e., signals, slots, do not apply)
            self.processes[process_]["signals"].append(slot)
            self.logger.info("Registered slot ``%s`` for process ``%s``" % (slot.__name__, process_.name))


    def __process_GetByName(self, name):
        """Returns a tuple (process, data) or (None, None)."""
        for (key, value) in self.processes.iteritems():
            if key.name == name:
                return (key, value)
        return (None, None)
        #raise AssertionError("""Process named "%s" not found!""" % name) 
    
    def process_GetById(self, id):
        """Returns a tuple (process, data) or raises AssertionError"""
        with self.L:
            for (proc, value) in self.processes.iteritems():
                if proc.id == id:
                    return (proc, value)
            return (None, None)
            #raise AssertionError("""Process id %d not found!""" % id)     

    def _Cycle(self):
        flagEmpty = False
        while not flagEmpty:
            try:
                (id, token, data) = self.processQueue.get(False)
                self._ProcessMessage(id, token, data)
            except Empty:
                flagEmpty = True
        time.sleep(0.2)
        
    def _ProcessMessage(self, id, token, data):
        """Processes message sent by process.
        
        Handled messages (with respective actions):
            Pong: Will log data
            NewData: passes new data information to na.vala (the latter will
                eventually emit a NewData signal). data is a dict, see
                navalasystem.System.NewData for reference
            Error: critical/fatal error in process, data is an Exception
            Unregister: process is asking self to finish and unregister it
            
        If message is unhandled, will check for "signals" within process data,
        and if finds any, will emit the signal with a list [token, data] as
        parameter.
        """
        (proc, procData) = self.process_GetById(id)
        self.logger.info("""Received message "%s" from process "%s"."""
                         % (token, proc.name))
        if token == "Pong":
            self.logger.info("PONG (#%d): %s" % (id, data))
        elif token == "NewData":
            na.vala.EmitNewData(data)
        elif token == "Error":
            self.logger.exception("Error from process #%d - %s" %
                                  (id, proc.name))
            self.logger.exception(data)
        elif token == "Unregister":
            self.process_Unregister(proc)
        else:
# TODO: gonna try using callbacks because seems that creating signals on-the-fly is more complicated than I thought. the TODO is because the variables have wrong names now (i.e., signals, slots, do not apply)
            if not misc.isempty(procData["signals"]):
                for signal in procData["signals"]:
                    signal([token, data])
            else:
                self.logger.debug("Unhandled message: \"%s\"" % token)


    def ExitAll(self):
        """Tells all processes to exit and waits..."""
        for item in self.processes:
            item.Exit()
        for item in self.processes:
            item.join()

